Skip to content

fix(agents): Record whose work a child agent does in its session log - #2181

Open
sentry-junior[bot] wants to merge 1 commit into
mainfrom
fix/child-task-on-behalf-of
Open

sentry-junior[bot] wants to merge 1 commit into
mainfrom
fix/child-task-on-behalf-of

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Supersedes #2175.

Problem

A watch, timer, or result Turn of a child agent runs as the event system Actor. Guardian read the root Conversation as that Run Actor. So the root user's own messages were labeled other person:, and Guardian blocked routine steps on the child's own work, such as a 👍 on review feedback or resolving a thread on the child's own PR.

Design

Whose work a child does is now session-log metadata. It is not a column, and it is not a field that only subagents have.

  • Message provenance gets one optional field, onBehalfOf: Actor. actor still means "who wrote this", and agent-written text still has no actor, so the fix(agents): Never take parent agent text as user approval in a child #2147 protection holds. onBehalfOf means "whose work this instruction serves". Only the runtime sets it.
  • spawnAgent and messageAgent put the calling Turn's person in the meta of the task message event that they already write into the child:
    • Root Turn: its Actor (the requester, not the thread owner). For an Automation run, this is the run's system Actor, so automation: authority still works.
    • Child Turn: its own onBehalfOf.
    • Root Event Turn (event system Actor): no stamp. A message keeps the child's earlier person. A new child has none.
  • When the agent invocation worker builds a child Run, it reads the stamp of its own task. Without a stamp, it uses onBehalfOf from the child's latest instruction user_message (loadLatestInstruction). The Turn writes the value into its own instruction provenance, so watch, timer, and result Turns keep it. Provenance passes unchanged through projection, compaction, and handoff.
  • Guardian's readRootConversation reads as onBehalfOf, not as the Run Actor. The Run Actor, credential context, CHILD_AGENT_USER_INTENT, and the Guardian prompt do not change.
  • A queued parent message steers a running child only for the same onBehalfOf. A message with no stamp keeps the running value.

on-behalf-of.ts owns these rules. agent-invocations/README.md documents them.

Why this replaces #2175

#2175 added turn_started.taskActor, subagent_started.causingTurn, a watch turnId, a mailbox watchTurnId, and resolveCausingTurn, which walked back from each Turn to its cause. Its audit found two problems. Batched event input from different people produced "no person". Watch replacement changed the creator.

This PR removes all of that. Each Turn does not find its cause. The person is fixed when a parent gives a task, and it stays in the child's log until a new task changes it. Event batching and watch ownership have no effect on it.

Also fixed (from #2175)

Nested results and messages took the Actor and credentials of the child's newest agent invocation. That row can be a timer Turn, so timer grants could pass to later work. child-input.ts now uses the child's latest task from spawnAgent or messageAgent. The regression test is in agent-invocation-nested.test.ts.

Legacy data

A child that started before this deploy has no onBehalfOf. On its event Turns, Guardian sees no user: lines until a new task stamps one. There is no fallback to the Run Actor, the thread owner, or actor_json. This is on purpose: when the cause is not known, there is no person.

Evals

The held Guardian eval in child-agent-approval.eval.ts now has two cases: a PR-watch Turn in a child marks feedback addressed and resolves the handled thread. I could not run it in the sandbox because no model or Roach credentials were available, so it needs a recorded run in CI.

via David Cramer.

--

View Junior Session [Sentry]

A watch, timer, or result Turn of a child agent runs as the event system
Actor. Action review used that Run Actor to read the root Conversation,
so the root user's own requests were labeled as another person, and
routine steps on the child's own work were blocked.

spawnAgent and messageAgent now put the calling Turn's person in the
meta of the task message that they write into the child, as
onBehalfOf. Each child Turn saves it in the provenance of its
instruction, and a Turn without a stamped task keeps the value of the
child's latest instruction. Action review reads the root Conversation
as onBehalfOf. The Run Actor, credentials, and child user intent do not
change, and agent text still has no author Actor.

Nested results and messages now take the Actor and credentials of the
child's latest task, not of its newest agent invocation, so the grants
of a timer Turn never pass to later work.

Supersedes #2175.

Co-Authored-By: David Cramer <david@sentry.io>
@vercel

vercel Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
junior-docs Ready Ready Preview Oct 17, 2026 1:48am UTC

Request Review

@sentry-evals

sentry-evals Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Sentry Evals

junior-guardian

Run Status Passed Failed Errored Cost Tokens Duration
Head Complete 68 0 6 $0.02 0 49.72s

Base commit f714c41 has no eval run to compare against.

dcramer added a commit that referenced this pull request Oct 31, 2026
Refs #2439

This adds the plan for #2439 in
`openspec/changes/actor-identity-end-state/`. It also adds **Owner** and
**Trusted author** to `TERMINOLOGY.md`, and says that an Automation has
a creator, not an owner (also in the dashboard Automation editor
README). There is no code change.

The plan fixes the target and the rules. It does not fix the
implementation. At the start of each step, the step re-checks the plan
against current `main` and settles its own details (SQL, keys, field
names, signatures, readers, tests) in its PR. A change to a decided rule
needs David's OK.

- `proposal.md`: scope, David's answers, plan decisions, open questions,
behavior changes, and risks.
- `design.md`: the model and rules, the current proposals that a step
must confirm, and the known gaps.
- `tasks.md`: how to use the plan, release rules, dependencies, and
steps 1–7. Each step has a goal, a done outcome, constraints, and what
to decide in that step.
- `specs/conversation-identity/spec.md`: the end-state requirements and
scenarios.

**Decided by David Cramer in the Slack thread**

- The Owner is the person who started the Conversation, or the system
for an automation. Owners never transfer.
- There should be no unknown actors on any Message from now on.
- The local CLI user can be the system Identity.
- A web user is the User they sign in with, through the normal User and
Identity tables.
- The schema should be normalized.
- Assistant output is just an event. It needs no author Identity.
- Web users always have an Identity. The mock viewer mocks one.
- Child spend rolls up to the root Owner.
- Slack bots have an Identity but no User.
- For an inbound event, Junior finds the correct Identity and verifies
it the same way as everywhere else. Junior ignores the instructions of
someone without access.
- Most events are system events, with the system Identity as author. PR
feedback and comments are authored by the person or bot who wrote them.
- Legacy `junior/<email>` rows: leave them as they are, unless a
migration path is clearly correct.
- Participants: "same as today: users." Verified event authors as
participants: "not right now maybe we will change our mind later."
- "also we'd allow list usernames in juniors config for github events
(so we could allow list some bots)"
- "call it trusted authors or sometihng", then: "yes "trusted" is what
we want here, but you can use the suffix that makes sense"
- "any sender who doesnt have the allowed domain email address or is
unknown isnt trusted. bots and or other users could be listed under
trusted"
- On what trusted text may do: "trusted authors are the same as normal
authors... its just that untrusted authors we dont run"

His answers to the 14 earlier proposals: (1) "bots need to be annotated
as bots, thats important"; (2) "you can find what makes sense"; (3)
"yes"; (4) "correct"; (5) "yes but an event *is* a message so"; (6)
"yes"; (7) "sure defer to you but needs to be correct"; (8) "yep"; (9)
"yep. and users have many identities associated to them"; (10) "thats
meaningless to me, make sure its good arch and ill trust you"; (11)
"yep"; (12) "idk up to you make it correct"; (13) "sure, we dont really
have "owners" on automations anyways"; (14) "ok".

**Plan decisions David handed off (2, 7, 10, 12).** Each is a current
proposal that its step confirms: the local auth bypass gets an Identity
that cannot match a production User (step 3); the Owner repair runs only
when it is clearly correct, after a dry run (step 4b); one system
Identity with a key that no plugin or OAuth link can write (#2443); the
mock viewer Identity is in memory only (step 3).

**Target, from those answers:** one Owner per root, set at insert, human
or system, never transferred. Children read the root Owner, and child
spend rolls up to it. One system Identity; "which system" is provenance.
Web users use their sign-in Identity; the mock viewer has a mock
Identity. Bots are kind `bot` with no User. Every new user Message has
an author Identity; assistant output has none. Events are user Messages
and only set the Message author. An event author is trusted when Junior
verifies them on an allowed domain or their login is on
`trustedAuthors`; untrusted or unknown authors do not run. Event authors
are not participants. Legacy rows stay unless a migration is clearly
correct.

**Release rules:** each step ships alone and is safe under a rolling
deploy. Readers ship before writers. A column drop ships one release
after the code stops using it. No production backfill without explicit
approval, and a dry run before any repair.

**Steps:** (1) one Actor-to-Identity mapping, built in #2443 (in
review); (2) carry the Identity id; (3) real Identities and the trust
rule; (4a) write the Owner; (4b) backfill, enforce, and read the Owner
(riskiest); (5) require authors and retire stored Actors; (6) stop using
the old columns; (7) drop them. #2441 before 2, #2260 before 3, #2382
before 4a, #2181 before 5.

**Open questions for David:**
1. Mixed event batch: are untrusted entries dropped, or kept as context
that does not start a run? Plan reading: dropped.
2. Does Cursor Bugbot go on `trustedAuthors`? Plan reading: not listed
yet, so its reviews stop starting runs when step 3 ships.

**Behavior changes:** automation-run spend leaves personal spend. A
human who replies to an automation Conversation becomes a participant,
not the Owner. Child spend goes to the root Owner. Trusted author text
counts like a chat message; untrusted or unknown authors do not run. The
participant list stops showing system Identities. Locations show one row
per User.

<!-- junior-request-attribution:start -->
via **David Cramer**.
<!-- junior-request-attribution:end -->

<!-- junior-session-footer:start -->
<!-- junior-conversation-id:agent%3Af31d0b3589d3ee280ff739bacb901c33 -->

--

[View
Session](https://junior-prod.sentry.dev/conversations/agent%3Af31d0b3589d3ee280ff739bacb901c33)
[[Sentry]](https://sentry.sentry.io/explore/conversations/agent%3Af31d0b3589d3ee280ff739bacb901c33/?project=4510944073809921)

<!-- junior-session-footer:end -->

---------

Co-authored-by: sentry-junior[bot] <264270552+sentry-junior[bot]@users.noreply.github.com>
Co-authored-by: David Cramer <david@sentry.io>

This branch was successfully deployed

1 active deployment
Preview – junior-docs — 0193f9eb Deployed Oct 17, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: high PR risk score: high

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants